using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

namespace Wolfpack.Periscope.Core.Infrastructure
{
    /// <summary>
    /// This will scan all the assemblies (*.dll|exe) in a given folder for a given set 
    /// of types and will return all the Type(s) that implement the interfaces specified
    /// </summary>
    public class TypeDiscovery
    {
        private static readonly string[] FilesToExclude;
        private static readonly string BinFolder;

        static TypeDiscovery()
        {
            FilesToExclude = new[]
            {
                "DotLiquid.dll",
                "Microsoft.AspNet.SignalR.Core.dll",
                "Nancy.dll",
                "Nancy.Hosting.Self.dll",
                "Newtonsoft.Json.dll",
                "Omu.ValueInjecter.dll",
                "Owin.dll"
            };

            BinFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
        }

        /// <summary>
        /// Simple helper to locate a single type in *.dll|exe
        /// </summary>
        /// <param name="matchingTypes"></param>
        /// <returns></returns>
        public static bool Discover<T>(out Type[] matchingTypes)
        {
            return Discover<T>(t => true, out matchingTypes);
        }

        /// <summary>
        /// Simple helper to locate a single type in *.dll|exe
        /// </summary>
        /// <param name="matchingTypes"></param>
        /// <param name="filter"></param>
        /// <returns></returns>
        public static bool Discover<T>(Predicate<Type> filter, out Type[] matchingTypes)
        {
            return Discover(typeof(T), filter, out matchingTypes);
        }

        /// <summary>
        /// Simple helper to locate matching types in *.dll|exe
        /// </summary>
        /// <param name="matchingTypes"></param>
        /// <param name="interfaceType"> </param>
        /// <returns></returns>
        public static bool Discover(Type interfaceType, out Type[] matchingTypes)
        {
            return Discover(interfaceType, x => true, out matchingTypes);
        }

        public static bool Discover(Type interfaceType, Predicate<Type> filter, out Type[] matchingTypes)
        {
            return new TypeDiscovery().Locate(interfaceType, filter, out matchingTypes);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="interfaceType"></param>
        /// <param name="matchingTypes"></param>
        /// <returns></returns>
        public bool Locate(Type interfaceType, out Type[] matchingTypes)
        {
            return Locate(interfaceType, x => true, out matchingTypes);
        }

        public bool Locate(Type interfaceType, Predicate<Type> filter, out Type[] matchingTypes)
        {
            var candidates = Directory.GetFiles(BinFolder, "*.*", SearchOption.TopDirectoryOnly)
                .Where(f => f.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) ||
                    f.EndsWith(".exe", StringComparison.OrdinalIgnoreCase));
            var filesToScan = candidates.Except(FilesToExclude, StringComparer.OrdinalIgnoreCase);

            var implementingTypes = new List<Type>();

            foreach (var assemblyFile in filesToScan)
            {
                try
                {
                    var assembly = Assembly.LoadFrom(assemblyFile);
                    var matches = assembly.GetExportedTypes()
                        .Where(t => filter(t) && !t.IsAbstract &&
                                    interfaceType.IsAssignableFrom(t)).ToList();

                    if (matches.Any())
                        implementingTypes.AddRange(matches);
                }
                catch
                {
                }                
            }

            matchingTypes = implementingTypes.ToArray();
            return (matchingTypes.Length > 0);
        }
    }
}